<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" />
        <meta
            name="viewport"
            content="width=device-width, minimal-ui, viewport-fit=cover, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no"
        />
        <link rel="icon" type="image/png" href="assets/favicon.png" />

        <title>OGL • Instancing</title>
        <link href="assets/main.css" rel="stylesheet" />
    </head>
    <body>
        <div class="Info">Instancing with GPU Picking. Model by Google Poly</div>
        <script type="module">
            import { Renderer, Camera, RenderTarget, Vec3, Transform, Texture, Program, Box, Mesh, NormalProgram } from '../src/index.mjs';

            const vertex = /* glsl */ `
                attribute vec2 uv;
                attribute vec3 position;
                attribute vec3 normal;

                // Add instanced attributes just like any attribute
                attribute vec3 offset;
                attribute vec3 random;
                attribute vec4 id;

                uniform mat4 modelViewMatrix;
                uniform mat4 projectionMatrix;
                uniform float uTime;

                varying vec2 vUv;
                varying vec3 vNormal;
                varying vec4 vId;

                void rotate2d(inout vec2 v, float a){
                    mat2 m = mat2(cos(a), -sin(a), sin(a),  cos(a));
                    v = m * v;
                }

                void main() {
                    vUv = uv;
                    vId = id;
                    vNormal = normal;
                    
                    // copy position so that we can modify the instances
                    vec3 pos = position;
                    
                    // scale first
                    pos *= 0.9 + random.y * 0.2;
                    
                    // rotate around y axis
                    rotate2d(pos.xz, random.x * 6.28 + 4.0 * uTime * (random.y - 0.5));
                    
                    pos += offset;
                    
                    gl_Position = projectionMatrix * modelViewMatrix * vec4(pos, 1.0);
                }
            `;

            const fragment = /* glsl */ `
                precision highp float;

                uniform float uTime;

                // Re-use same program to render pick-texture
                uniform bool uTargetRender;

                varying vec2 vUv;
                varying vec4 vId;
                varying vec3 vNormal;

                void main() {
                    if (uTargetRender) {
                        gl_FragColor = vId;
                        return;
                    }

                    vec3 normal = normalize(vNormal);
                    float lighting = dot(normal, normalize(vec3(-0.3, 0.8, 0.6)));
                    gl_FragColor.rgb = vec3(0.2, 0.8, 1.0) + lighting * 0.1;
                    gl_FragColor.a = 1.0;
                }
            `;

            {
                const renderer = new Renderer({ dpr: 2 });
                const gl = renderer.gl;
                document.body.appendChild(gl.canvas);
                gl.clearColor(1, 1, 1, 1);

                const camera = new Camera(gl, { fov: 15 });
                camera.position.z = 15;

                let target;

                function resize() {
                    renderer.setSize(window.innerWidth, window.innerHeight);
                    camera.perspective({ aspect: gl.canvas.width / gl.canvas.height });
                    target = new RenderTarget(gl);
                }
                window.addEventListener('resize', resize, false);
                resize();

                const scene = new Transform();

                const program = new Program(gl, {
                    vertex,
                    fragment,
                    uniforms: {
                        uTime: { value: 0 },
                        uTargetRender: { value: 0 },
                    },
                });

                let mesh,
                    highlight,
                    objects = [];
                {
                    const num = 20;

                    let offsetData = new Float32Array(num * 3);
                    let randomData = new Float32Array(num * 3);
                    let idData = new Float32Array(num * 4);
                    for (let i = 0; i < num; i++) {
                        const offset = [Math.random() * 2 - 1, Math.random() * 2 - 1, Math.random() * 2 - 1];
                        offsetData.set(offset, i * 3);

                        let id = i + 1;
                        idData.set(
                            [((id >> 0) & 0xff) / 0xff, ((id >> 8) & 0xff) / 0xff, ((id >> 16) & 0xff) / 0xff, ((id >> 24) & 0xff) / 0xff],
                            i * 4
                        );

                        const random = [Math.random(), Math.random(), Math.random()];
                        randomData.set(random, i * 3);

                        objects.push({ id, offset, random });
                    }

                    const geometry = new Box(gl, {
                        width: 0.2,
                        height: 0.2,
                        depth: 0.2,
                        attributes: {
                            offset: { instanced: 1, size: 3, data: offsetData },
                            random: { instanced: 1, size: 3, data: randomData },
                            // Add id data. This way we can recognize the instance later.
                            id: { instanced: 1, size: 4, data: idData },
                        },
                    });

                    mesh = new Mesh(gl, { geometry, program });
                    mesh.setParent(scene);

                    // Create a single non-instanced copy
                    highlight = new Mesh(gl, { geometry, program: NormalProgram(gl) });
                    highlight.setParent(scene);
                    highlight.visible = false;
                }

                // Wrap in load event to prevent checks before page is ready
                window.addEventListener(
                    'load',
                    () => {
                        document.addEventListener('mousemove', move, false);
                        document.addEventListener('touchmove', move, false);
                    },
                    false
                );

                const mouse = new Vec3();
                function move(e) {
                    mouse.set(
                        (e.x * gl.canvas.width) / gl.canvas.clientWidth,
                        gl.canvas.height - (e.y * gl.canvas.height) / gl.canvas.clientHeight - 1
                    );
                }

                requestAnimationFrame(update);
                function update(t) {
                    requestAnimationFrame(update);
                    if (!mesh) return;

                    let time = t * 0.001;
                    program.uniforms.uTime.value = time;

                    // Draw to texture
                    mesh.program.uniforms.uTargetRender.value = 1;
                    gl.clearColor(0, 0, 0, 0);

                    // Render ids to render target
                    renderer.render({ scene: mesh, camera, target });

                    // Read pixel data from target
                    const data = new Uint8Array(4);
                    gl.readPixels(
                        mouse.x, // x
                        mouse.y, // y
                        1, // width
                        1, // height
                        gl.RGBA, // format
                        gl.UNSIGNED_BYTE, // type
                        data
                    ); // typed array to hold result

                    // Transform color back to id
                    const id = data[0] + (data[1] << 8) + (data[2] << 16) + (data[3] << 24);

                    if (id) {
                        // Restore same transformations as in shader
                        // but only for the higlighted item
                        let data = objects[id - 1];
                        let [rx, ry, rz] = data.random;
                        highlight.scale.set(0.9 + ry * 0.2).scale(1.005);
                        highlight.rotation.set(0);
                        highlight.rotation.y = rx * 6.28 + 4 * time * (ry - 0.5);
                        highlight.position.set(data.offset);
                        highlight.visible = true;
                    } else {
                        highlight.visible = false;
                    }

                    // Reset
                    mesh.program.uniforms.uTargetRender.value = 0;
                    program.uniforms.uTime.value = time;
                    gl.clearColor(1, 1, 1, 1);

                    renderer.render({ scene, camera });
                }
            }
        </script>
    </body>
</html>
